package cn.bugstack.trigger.job;

import cn.bugstack.domain.activity.model.valobj.ActivitySkuStockKeyVO;
import cn.bugstack.domain.strategy.model.valobj.StrategyAwardStockKeyVO;
import cn.bugstack.domain.strategy.service.IRaffleAward;
import cn.bugstack.domain.strategy.service.IRaffleStock;
import com.xxl.job.core.handler.annotation.XxlJob;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.util.List;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

/**
 * @author Fuzhengwei bugstack.cn @小傅哥
 * @description 更新奖品库存任务；为了不让更新库存的压力打到数据库中，这里采用了redis更新缓存库存，异步队列更新数据库，数据库表最终一致即可。
 * 
 * 单独更新库存，如果前后业务中断了，是否会影响最终结果的数据一致性。
 * 如库存更新后，之后业务中断了，是否会出现整个业务流程只是单方面的扣减了库存，没有之后的数据流向。
 * 
 * @create 2024-02-09 12:13
 */
@Slf4j
@Component()
public class UpdateAwardStockJob {

    @Resource
    private IRaffleStock raffleStock;
    @Resource
    private IRaffleAward raffleAward;
    @Resource
    private ThreadPoolExecutor executor;
    @Resource
    private RedissonClient redissonClient;

    /**
     * 本地化任务注解；@Scheduled(cron = "0/5 * * * * ?")
     * 分布式任务注解； @XxlJob("updateAwardStockJob")
     * 
     * 使用了 Micrometer 进行任务监控 @Timed
     * 使用 XXL-Job 分布式任务调度平台进行任务触发 @XxlJob
     */
    @XxlJob("updateAwardStockJob")
    public void exec() {
        // 为什么加锁？分布式应用N台机器部署互备，任务调度会有N个同时执行，那么这里需要增加抢占机制，谁抢占到谁就执行。完毕后，下一轮继续抢占。
        // 确保在集群环境下只有一个实例执行任务，防止重复更新。

        // 创建一个基于Redis的锁对象
        // getLock的工作原理：
        // 1、在Redis中：这个操作并不立即在Redis中创建任何对象，它只是在本地创建一个引用Redis键的RLock对象
        // 2、键名称：对应Redis中的键为"big-market-updateAwardStockJob"
        // 3、多服务同时调用：多个服务同时调用getLock获取的是指向同一个Redis键的不同RLock实例对象
        // 4、不会冲突：这一步只是获取引用，不会对Redis产生写入操作，因此不会有冲突
        // 关键点：getLock只是创建了一个客户端对象，这个对象知道要操作Redis中的哪个键，但并没有实际获取锁。
        RLock lock = redissonClient.getLock("big-market-updateAwardStockJob");
        try {
            // 尝试获取锁，最多等待3秒。锁超时时间设为 3 秒，防止线程死锁
            // 锁信息会存储在Redis中（key为锁名称，value包含线程标识和过期时间）
            //
            // 实现原理：通过Redis的原子操作和Lua脚本实现
            // Redis操作：当执行tryLock时，Redisson会向Redis发送获取锁的命令
            // 具体流程：
            //          使用SET NX EX命令尝试设置键值对
            //          值包含线程标识和锁过期时间
            //          如果设置成功（返回OK），表示获取锁成功
            //          如果设置失败，表示锁已被其他服务持有
            // 处理竞争：当多个服务同时尝试获取锁时，Redis的单线程模型确保只有一个服务能成功
            // 多服务争抢：其他服务会收到获取锁失败的响应（tryLock返回false）
            // 3. isLocked和isHeldByCurrentThread
            boolean isLocked = lock.tryLock(3, 0, TimeUnit.SECONDS);
            if (!isLocked) return;

            List<StrategyAwardStockKeyVO> strategyAwardStockKeyVOS = raffleAward.queryOpenActivityStrategyAwardList();
            if (null == strategyAwardStockKeyVOS) return;
            for (StrategyAwardStockKeyVO strategyAwardStockKeyVO : strategyAwardStockKeyVOS) {
                // 使用线程池的异步执行方式
                // execute()方法会将任务提交到线程池，立即返回
                // 主线程不会等待任务完成
                // 多个任务会并发执行
                executor.execute(() -> {
                    try {
                        StrategyAwardStockKeyVO queueStrategyAwardStockKeyVO = raffleStock.takeQueueValue(strategyAwardStockKeyVO.getStrategyId(), strategyAwardStockKeyVO.getAwardId());
                        if (null == queueStrategyAwardStockKeyVO) return;
                        log.info("定时任务，更新奖品消耗库存 strategyId:{} awardId:{}", queueStrategyAwardStockKeyVO.getStrategyId(), queueStrategyAwardStockKeyVO.getAwardId());
                        raffleStock.updateStrategyAwardStock(queueStrategyAwardStockKeyVO.getStrategyId(), queueStrategyAwardStockKeyVO.getAwardId());
                    } catch (InterruptedException e) {
                        log.error("定时任务，更新奖品消耗库存失败 strategyId:{} awardId:{}", strategyAwardStockKeyVO.getStrategyId(), strategyAwardStockKeyVO.getAwardId());
                    }
                });
            }
        } catch (Exception e) {
            log.error("定时任务，更新奖品消耗库存失败", e);
        } finally {
            // isLocked()：检查锁是否被任何线程持有
            //      检查Redis中锁是否存在
            //      向Redis发送命令查询锁键是否存在
            //      这个信息是分布式共享的，反映的是Redis中实际锁的状态
            // 
            // isHeldByCurrentThread()：检查锁是否被当前线程持有
            //      检查当前线程是否持有锁
            //      这是基于本地信息和Redis信息的组合判断
            //      本地记录自己的线程ID，然后检查Redis中存储的线程ID是否匹配
            //      部分逻辑在本地执行，部分需要查询Redis
            // 
            // RLock的 commandExecutor 属性 是 Redisson 与 Redis 交互的“桥梁”，屏蔽了底层网络、序列化、集群路由等复杂性，
            // 为分布式锁的实现提供高效可靠的命令执行能力。若需深入分析，可查看 RedissonCommandExecutor 的源码逻辑。
            if (lock.isLocked() && lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }

}
